Implementing Integration and End-to-End Testing Fundamentals
Course: React Native Training
Topic: Implementing Integration and End-to-End Testing Fundamentals
Course Link: React Native Training at JustAcademy
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Integration and End-to-End Testing
Testing is an essential part of building reliable React Native applications. It helps developers verify that components, services, navigation, APIs, and other application features work correctly. While individual component tests focus on isolated UI behavior, integration and end-to-end testing examine how different parts of an application work together.
Integration testing verifies that multiple components, modules, or services cooperate correctly. For example, it can check whether a login screen sends entered credentials to an authentication service and displays the resulting response.
End-to-end (E2E) testing verifies a complete user workflow from beginning to end. For example, an E2E test might launch a mobile app, enter login details, press the Login button, and verify that the user reaches the home screen.
These testing approaches complement unit and component tests. Together, they help identify problems at different levels of an application's architecture.
1.1 Learning Objectives
- Understand the purpose of integration testing and end-to-end testing.
- Compare unit, component, integration, and E2E testing.
- Identify the tools commonly used for React Native testing.
- Set up a basic testing strategy for a React Native project.
- Test interactions between components, services, and APIs.
- Implement complete user workflows using an E2E testing tool.
- Handle asynchronous operations, test data, and application state.
- Recognize common testing failures and troubleshoot them.
- Apply testing best practices to real-world React Native projects.
2. Understanding the Different Levels of Testing
React Native applications can be tested at several levels. Each level serves a different purpose and provides a different degree of confidence.
| Testing Level |
Purpose |
Example |
| Unit testing |
Tests a small function or isolated piece of logic. |
Checking whether an email validation function accepts a valid email address. |
| Component testing |
Tests a UI component and its observable behavior. |
Checking whether a button invokes its callback when pressed. |
| Integration testing |
Tests multiple components or modules working together. |
Checking whether a form submits data to a mocked service and displays a result. |
| End-to-end testing |
Tests a complete workflow in a running application. |
Opening the app, logging in, navigating to a product, and adding it to a cart. |
| Manual testing |
Allows a person to explore usability and device-specific behavior. |
Checking how a keyboard, permissions prompt, or screen reader behaves on a real device. |
2.1 How the Testing Levels Work Together
Unit tests usually run quickly and are useful for testing many small conditions. Component tests verify visible UI behavior. Integration tests verify communication between modules, while E2E tests provide confidence that important workflows operate correctly in a realistic application environment.
A balanced testing strategy uses many fast tests and a smaller set of carefully selected E2E tests for important user journeys.
3. What Is Integration Testing?
Integration testing verifies that separate parts of an application cooperate as intended. These parts may include React Native components, context providers, state management, navigation, API clients, authentication modules, storage services, and other dependencies.
3.1 Example of Integration Testing
Consider a registration screen with an email input, a password input, a submission button, and a registration service. A component test might check the input field by itself. An integration test can verify that:
- The user enters registration information.
- The form validates the information.
- The submit action calls the registration service.
- The service response is handled correctly.
- A success message or error message is displayed.
3.2 What Integration Tests Can Verify
- Components communicating through props and callbacks.
- Forms interacting with validation logic.
- State updates appearing in the UI.
- Context providers supplying data to child components.
- API services returning success or failure responses.
- Navigation actions being triggered correctly.
- Loading indicators appearing while requests are pending.
- Error messages appearing after failed operations.
- Local storage or application state being updated as expected.
3.3 Integration Testing Boundaries
Integration testing does not always require a real backend or a fully running native application. Many integration tests use React Native Testing Library with mocked external services. The purpose is to verify that the relevant application modules work together without introducing unnecessary dependencies on external systems.
4. What Is End-to-End Testing?
End-to-end testing evaluates an application through the same major steps a real user would follow. Tests interact with the running app and verify that the overall workflow produces the expected result.
Unlike a typical component test, an E2E test may involve a built application running on an emulator, simulator, or physical device. It can exercise navigation, forms, screen transitions, and integrations in a more realistic environment.
4.1 Example: E2E Login Workflow
- Launch the React Native application.
- Wait for the login screen to appear.
- Enter a valid email address.
- Enter a password.
- Press the Login button.
- Wait for authentication to complete.
- Verify that the home screen appears.
- Verify that the expected account information is displayed.
4.2 Features Commonly Tested with E2E Tests
- Application startup and initial navigation.
- Login, logout, and registration flows.
- Screen transitions and navigation stacks.
- Search, filtering, and product selection.
- Shopping cart and checkout flows.
- Profile editing and settings.
- Permission-dependent workflows.
- Offline and network-error experiences.
- Important workflows involving several screens.
5. Integration Testing vs. End-to-End Testing
| Aspect |
Integration Testing |
End-to-End Testing |
| Scope |
A group of related components, modules, or services. |
A complete user journey across the running app. |
| Execution environment |
Often a JavaScript test environment. |
Usually an emulator, simulator, or physical device. |
| External dependencies |
Frequently mocked or controlled. |
May use a test backend or controlled test environment. |
| Execution speed |
Generally faster. |
Generally slower because the app must launch and interact with the environment. |
| Failure diagnosis |
Usually easier to isolate to a smaller integration boundary. |
Can be harder because several layers may be involved. |
| Main objective |
Verify cooperation between related modules. |
Verify that critical user workflows function correctly. |
| Typical tools |
Jest and React Native Testing Library. |
Detox or another compatible mobile automation tool. |
6. Choosing the Right Testing Tools
The tools used for integration and E2E testing depend on the project's React Native version, architecture, build system, and testing requirements.
6.1 Jest
Jest is a JavaScript testing framework commonly used in React Native projects. It runs tests, provides assertions, supports mock functions, and helps control test setup.
Jest is useful for unit tests and many integration tests. By itself, it does not automate a complete mobile application on a simulator or physical device.
6.2 React Native Testing Library
React Native Testing Library provides utilities for rendering components, finding elements, and simulating interactions. It is useful for component tests and integration tests that exercise multiple connected UI elements.
6.3 Detox
Detox is a mobile E2E testing framework designed for React Native applications. It automates a running application and provides APIs for interacting with UI elements and checking expected states. Setup depends on the operating system, React Native version, native build configuration, and selected simulator or device.
6.4 Other Testing Approaches
Teams may use other mobile automation tools depending on their infrastructure and requirements. When choosing a tool, consider platform support, build integration, CI compatibility, maintenance cost, debugging capabilities, and the types of interactions that need to be tested.
| Tool |
Primary Role |
Common Use |
| Jest |
JavaScript test runner and assertion framework. |
Unit and integration tests. |
| React Native Testing Library |
Component rendering and interaction utilities. |
Component and UI integration tests. |
| Detox |
Mobile application automation. |
End-to-end tests on supported simulators, emulators, and devices. |
| CI platform |
Automates builds and test execution. |
Running tests whenever code changes are submitted. |
7. Preparing a React Native Testing Environment
Before implementing integration and E2E tests, make sure the application can run, its dependencies are installed, and the test configuration matches the project's framework versions.
7.1 Install Integration Testing Dependencies
For a compatible project, React Native Testing Library can be installed as a development dependency:
npm install --save-dev @testing-library/react-native
Jest and its React Native configuration are commonly provided by the project template. If they are not present, install the versions recommended for the project's React Native version rather than selecting versions independently.
7.2 Add a Test Script
A typical Jest-based project may define a test script in package.json:
{
"scripts": {
"test": "jest"
}
}
Run the tests using:
npm test
7.3 Organize the Test Files
src/
components/
LoginForm.js
LoginForm.test.js
services/
authService.js
screens/
LoginScreen.js
LoginScreen.test.js
e2e/
login.e2e.js
package.json
jest.config.js
The directory structure is an example. Adapt it to the conventions already used by the application and E2E framework.
8. Implementing a Basic Integration Test
Consider a login form that receives an authentication function through props. The form validates required fields, calls the authentication function, and displays a result. The integration test should verify the interaction between the inputs, button, callback, and resulting UI.
8.1 Create the Authentication Service Contract
// authService.js
export async function loginUser({ email, password }) {
// In a real application, call the authentication API here.
// Keep network logic outside the UI component.
return {
success: true,
user: { name: 'Asha' }
};
}
This service is illustrative. A production implementation should handle real authentication, network failures, and secure credential processing.
8.2 Create the Login Form Component
// LoginForm.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
export default function LoginForm({ onLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const handleLogin = async () => {
if (!email.trim() || !password) {
setMessage('Email and password are required');
return;
}
setMessage('Signing in...');
try {
const result = await onLogin({
email: email.trim(),
password
});
if (result.success) {
setMessage(`Welcome, ${result.user.name}`);
} else {
setMessage('Unable to sign in');
}
} catch (error) {
setMessage('Unable to sign in');
}
};
return (
<View>
<TextInput
accessibilityLabel="Email"
placeholder="Enter email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<TextInput
accessibilityLabel="Password"
placeholder="Enter password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Login" onPress={handleLogin} />
{message !== '' && <Text>{message}</Text>}
</View>
);
}
8.3 Test Successful Login Integration
// LoginForm.test.js
import React from 'react';
import {
render,
screen,
userEvent
} from '@testing-library/react-native';
import LoginForm from './LoginForm';
test('submits credentials and displays a welcome message', async () => {
const onLogin = jest.fn().mockResolvedValue({
success: true,
user: { name: 'Asha' }
});
const user = userEvent.setup();
render(<LoginForm onLogin={onLogin} />);
await user.type(
screen.getByLabelText('Email'),
'[email protected]'
);
await user.type(
screen.getByLabelText('Password'),
'ExamplePassword123'
);
await user.press(
screen.getByRole('button', { name: 'Login' })
);
expect(onLogin).toHaveBeenCalledWith({
email: '[email protected]',
password: 'ExamplePassword123'
});
expect(await screen.findByText('Welcome, Asha'))
.toBeOnTheScreen();
});
8.4 Test Validation and Failure Scenarios
test('does not submit empty credentials', async () => {
const onLogin = jest.fn();
const user = userEvent.setup();
render(<LoginForm onLogin={onLogin} />);
await user.press(
screen.getByRole('button', { name: 'Login' })
);
expect(
screen.getByText('Email and password are required')
).toBeOnTheScreen();
expect(onLogin).not.toHaveBeenCalled();
});
test('displays an error when authentication fails', async () => {
const onLogin = jest.fn().mockRejectedValue(
new Error('Network unavailable')
);
const user = userEvent.setup();
render(<LoginForm onLogin={onLogin} />);
await user.type(
screen.getByLabelText('Email'),
'[email protected]'
);
await user.type(
screen.getByLabelText('Password'),
'ExamplePassword123'
);
await user.press(
screen.getByRole('button', { name: 'Login' })
);
expect(await screen.findByText('Unable to sign in'))
.toBeOnTheScreen();
});
These tests verify that the form, validation logic, authentication callback, and displayed feedback cooperate correctly. The authentication service is mocked so the test does not depend on a real backend.
9. Integration Testing with API Services
Many React Native applications communicate with REST APIs, GraphQL services, or other backend systems. Integration tests can verify that the UI responds correctly to the data returned by an API client.
9.1 Important API States
- Loading: The request is in progress.
- Success: The response contains usable data.
- Empty result: The request succeeds but no data is available.
- Failure: The server returns an error or the request fails.
- Retry: The user can attempt the operation again when supported.
9.2 Example API-Driven Component
// ProfilePanel.js
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
export default function ProfilePanel({ getProfile }) {
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
let active = true;
getProfile()
.then(data => {
if (active) setProfile(data);
})
.catch(() => {
if (active) setError(true);
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, [getProfile]);
if (loading) return <Text>Loading profile...</Text>;
if (error) return <Text>Could not load profile</Text>;
if (!profile) return <Text>No profile available</Text>;
return (
<View>
<Text>{profile.name}</Text>
<Text>{profile.email}</Text>
</View>
);
}
In this example, getProfile is supplied through props. In a real application, it may be imported from an API service or obtained from a custom hook. If the function is recreated on every render, consider stabilizing its identity to avoid unnecessary repeated requests.
9.3 Test the Successful Response
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import ProfilePanel from './ProfilePanel';
test('displays profile information after loading', async () => {
const getProfile = jest.fn().mockResolvedValue({
name: 'Asha',
email: '[email protected]'
});
render(<ProfilePanel getProfile={getProfile} />);
expect(await screen.findByText('Asha'))
.toBeOnTheScreen();
expect(screen.getByText('[email protected]'))
.toBeOnTheScreen();
});
9.4 Test API Failure
test('displays an error when the profile request fails', async () => {
const getProfile = jest.fn().mockRejectedValue(
new Error('Request failed')
);
render(<ProfilePanel getProfile={getProfile} />);
expect(await screen.findByText('Could not load profile'))
.toBeOnTheScreen();
});
Integration tests should not depend on a live production API. Use controlled responses or a dedicated test backend so that the same test produces consistent results across runs.
10. Testing State Management and Context
Applications may use React Context, Redux, Zustand, or another state management approach. Integration testing helps confirm that a change in shared state is reflected in the appropriate components.
10.1 Example: Theme Context
// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(current =>
current === 'light' ? 'dark' : 'light'
);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
10.2 Create a Component That Uses the Context
// ThemePanel.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useTheme } from './ThemeContext';
export default function ThemePanel() {
const { theme, toggleTheme } = useTheme();
return (
<View>
<Text>Current theme: {theme}</Text>
<Button title="Toggle theme" onPress={toggleTheme} />
</View>
);
}
10.3 Test the Provider and Consumer Together
import React from 'react';
import {
render,
screen,
userEvent
} from '@testing-library/react-native';
import { ThemeProvider } from './ThemeContext';
import ThemePanel from './ThemePanel';
test('updates the displayed theme when toggled', async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemePanel />
</ThemeProvider>
);
expect(screen.getByText('Current theme: light'))
.toBeOnTheScreen();
await user.press(
screen.getByRole('button', { name: 'Toggle theme' })
);
expect(screen.getByText('Current theme: dark'))
.toBeOnTheScreen();
});
This test checks the interaction between the provider, the shared state, and the consuming component. Similar tests can be written for authentication context, shopping carts, language preferences, and other shared application state.
11. Integration Testing with Navigation
Navigation integration tests verify that user actions trigger the correct screen transition or update the appropriate navigation state. Depending on the goal, a test can mock a navigation callback or render a real navigator with test screens.
11.1 Testing a Navigation Callback
import React from 'react';
import { Button } from 'react-native';
export default function OpenSettingsButton({ onOpenSettings }) {
return (
<Button
title="Open settings"
onPress={onOpenSettings}
/>
);
}
test('requests opening the settings screen', async () => {
const onOpenSettings = jest.fn();
const user = userEvent.setup();
render(
<OpenSettingsButton onOpenSettings={onOpenSettings} />
);
await user.press(
screen.getByRole('button', { name: 'Open settings' })
);
expect(onOpenSettings).toHaveBeenCalledTimes(1);
});
11.2 Testing a Real Navigator
When the objective is to verify screen transitions, render the actual navigator with a suitable navigation container and test routes. Then perform the user action and verify the destination screen's content.
For example, an integration test can confirm that pressing a Profile button from the home screen displays the Profile screen. The test should use the navigation setup appropriate to the installed navigation library and application configuration.
12. Introduction to End-to-End Testing with Detox
Detox is a mobile automation framework that can drive a React Native application in a supported device environment. It can launch the app, locate UI elements, interact with them, and verify visible results.
12.1 Why Use Detox?
- Test complete workflows instead of isolated components.
- Verify that major screens and navigation paths work together.
- Check real application builds in a device-like environment.
- Automate repetitive regression tests.
- Run important workflows as part of continuous integration.
12.2 Prerequisites
- A working React Native project.
- A compatible Node.js and package manager setup.
- Required native development tools for the selected platform.
- An Android emulator or iOS simulator supported by the project's environment.
- A compatible Detox version and its required configuration.
- A development or test build that can be launched by the automation framework.
Detox setup can differ significantly between Android and iOS and between React Native versions. Follow the official installation instructions for the exact combination used by the project.
13. Preparing an App for E2E Tests
Reliable E2E tests need stable selectors and predictable application data. A test should be able to identify the intended element without depending on visual position or fragile text that changes frequently.
13.1 Add Test IDs to Important Elements
import React from 'react';
import { View, Text, TextInput, Button } from 'react-native';
export default function LoginScreen() {
return (
<View>
<Text>Sign in</Text>
<TextInput
testID="email-input"
accessibilityLabel="Email"
placeholder="Enter email"
/>
<TextInput
testID="password-input"
accessibilityLabel="Password"
placeholder="Enter password"
secureTextEntry
/>
<Button
testID="login-button"
title="Login"
onPress={() => {}}
/>
</View>
);
}
The component above is a simplified example for demonstrating selectors. In a real app, connect the controls to the authentication logic and render the appropriate destination screen after success.
13.2 Why Stable Selectors Matter
- They reduce dependence on layout and visual hierarchy.
- They help tests find the intended element consistently.
- They make test failures easier to diagnose.
- They reduce the likelihood of breaking tests after small design changes.
Use accessibility labels and roles where appropriate. Use test IDs when a stable identifier is helpful and a meaningful user-facing query is insufficient.
14. Writing an E2E Login Test
An E2E login test should interact with the running application, enter test credentials, submit the form, and verify the expected destination screen. The following example illustrates a common Detox test pattern. It assumes that the project has already been configured for Detox.
14.1 Example Test File
// e2e/login.e2e.js
describe('Login flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('allows a user to sign in', async () => {
await expect(element(by.id('email-input')))
.toBeVisible();
await element(by.id('email-input'))
.typeText('[email protected]');
await element(by.id('password-input'))
.typeText('TestPassword123');
await element(by.id('login-button'))
.tap();
await expect(element(by.id('home-screen')))
.toBeVisible();
});
});
Important: The example uses common Detox APIs, but the exact setup, command syntax, and test lifecycle depend on the installed Detox version and project configuration. The test also assumes that the application has a working test authentication flow and that the home screen exposes testID="home-screen".
14.2 Understanding the Test
device.launchApp() launches the application.
device.reloadReactNative() reloads the React Native application where supported by the configured environment.
element(by.id(...)) identifies an element using its test ID.
typeText() enters text into a field.
tap() performs a tap on the selected element.
toBeVisible() verifies that the expected element is visible.
15. E2E Testing of Form Validation
Validation is a good E2E testing target because it affects a complete user interaction. A test can verify that submitting an empty form shows an error and does not navigate to the next screen.
describe('Login validation', () => {
beforeEach(async () => {
await device.reloadReactNative();
});
it('shows an error when required fields are empty', async () => {
await element(by.id('login-button')).tap();
await expect(
element(by.text('Email and password are required'))
).toBeVisible();
await expect(element(by.id('home-screen')))
.not.toBeVisible();
});
});
The displayed validation text must match the actual component. Ensure that the home screen is not present on the initial login screen, or adjust the assertion to match the application's real navigation behavior.
16. E2E Testing of Navigation
Navigation workflows are important because incorrect transitions can prevent users from completing common tasks. An E2E test can verify that a user moves from one screen to another and can return to the expected screen.
16.1 Example Workflow
- Launch the application.
- Verify that the home screen appears.
- Press the Profile button.
- Verify that the Profile screen appears.
- Press the Settings button.
- Verify that the Settings screen appears.
- Use the Back control.
- Verify that the expected previous screen appears.
16.2 Example Test
describe('Profile navigation', () => {
it('opens the profile and settings screens', async () => {
await device.launchApp();
await element(by.id('profile-button')).tap();
await expect(element(by.id('profile-screen')))
.toBeVisible();
await element(by.id('settings-button')).tap();
await expect(element(by.id('settings-screen')))
.toBeVisible();
});
});
Use stable selectors on the actual screens and controls. Add a reset or navigation setup step when the app may start in a different state after previous tests.
17. E2E Testing of API-Driven Screens
API-driven screens can be difficult to test if they rely on unpredictable live services. A test environment should provide controlled data and consistent behavior.
17.1 Recommended Approaches
- Use a dedicated test backend with predictable accounts and records.
- Seed the test database before running the test suite.
- Reset test data between tests when necessary.
- Use a controlled mock server when testing specific network responses.
- Avoid using real customer data or production credentials.
- Keep test credentials in appropriate secure configuration rather than committing secrets.
17.2 Example Product Workflow
describe('Product browsing', () => {
it('opens a product and adds it to the cart', async () => {
await device.launchApp();
await expect(element(by.id('product-list')))
.toBeVisible();
await element(by.id('product-notebook')).tap();
await expect(element(by.id('product-detail-screen')))
.toBeVisible();
await element(by.id('add-to-cart-button')).tap();
await expect(element(by.id('cart-count')))
.toHaveText('1');
});
});
This example assumes the application provides the specified test IDs and that a known product exists in the test environment. The test should also define how the cart is reset so that repeated runs produce the same result.
18. Handling Asynchronous Operations in E2E Tests
Mobile applications often perform asynchronous operations, such as network requests, database queries, animations, and navigation transitions. E2E tests must account for these operations without relying on arbitrary delays.
18.1 Prefer Condition-Based Waiting
Prefer the E2E framework's built-in synchronization and expectation mechanisms. For example, wait for a success message or destination screen to become visible instead of adding a fixed delay after every tap.
await element(by.id('save-button')).tap();
await expect(element(by.id('save-success-message')))
.toBeVisible();
18.2 Avoid Arbitrary Timeouts
Fixed delays can make tests slower and less reliable. A short delay may be insufficient on a slow device, while a long delay wastes execution time. Use explicit waits and assertions based on the expected application state whenever possible.
18.3 Test Loading States
When loading feedback is important, verify that it appears while the operation is pending and disappears after the result arrives. The test environment must control the operation's timing sufficiently for these assertions to be meaningful.
19. Test Data Management
Test data affects reliability. If one test leaves behind data that another test expects not to exist, the suite can become inconsistent and difficult to debug.
19.1 Common Test Data Strategies
- Seed data: Create known records before testing.
- Reset state: Clear application or backend state before a workflow.
- Dedicated accounts: Use non-production accounts specifically for tests.
- Deterministic fixtures: Use predefined input values and responses.
- Unique records: Generate distinct test records when parallel execution requires isolation.
- Cleanup: Remove temporary data when it is safe and practical.
19.2 Example Test Data
const testUser = {
email: '[email protected]',
password: 'TestPassword123'
};
const testProduct = {
id: 'product-001',
name: 'Notebook',
price: 5
};
These values are illustrative. Do not use production credentials or sensitive personal information in test fixtures.
20. Testing Permissions and Device Features
React Native applications may use camera access, location services, notifications, photo libraries, biometrics, or other native capabilities. These features need careful testing because their behavior can depend on the operating system, device settings, and permission state.
20.1 What to Test
- How the app behaves when permission is granted.
- How the app behaves when permission is denied.
- Whether a useful message appears when a feature is unavailable.
- Whether users can retry or open settings when supported.
- Whether the app handles missing hardware or unsupported capabilities.
20.2 Example Permission Scenarios
| Scenario |
Expected Behavior |
| Permission granted |
The feature becomes available. |
| Permission denied |
The app displays an appropriate explanation or fallback. |
| Permission permanently denied |
The app explains the limitation and may direct the user to settings. |
| Feature unavailable |
The app avoids crashing and provides a suitable alternative. |
Component tests can verify messages and conditional rendering. E2E or manual device tests are needed to verify real operating-system permission behavior.
21. Handling Test Flakiness
A flaky test sometimes passes and sometimes fails without a relevant code change. Flaky E2E tests can reduce confidence in the test suite and slow development.
21.1 Common Causes
- Unstable network conditions.
- Uncontrolled backend or database state.
- Fixed delays that do not match the actual operation time.
- Ambiguous or unstable selectors.
- Animations or transitions that are not synchronized.
- Tests depending on execution order.
- Shared accounts or data modified by parallel tests.
- Unreliable emulator or simulator configuration.
21.2 Reducing Flakiness
- Use stable test IDs or accessible selectors.
- Wait for meaningful UI conditions rather than arbitrary durations.
- Keep test data predictable and isolated.
- Reset application state between independent workflows.
- Make tests independent of their execution order.
- Mock external services when testing a controlled response.
- Use a stable simulator or emulator configuration.
- Capture logs and screenshots when a failure occurs.
- Investigate repeated failures rather than simply rerunning tests indefinitely.
22. Debugging Integration and E2E Tests
When a test fails, identify which layer is responsible before changing the test or application code. A failure may originate in the component, service, navigation configuration, test setup, native build, or external environment.
22.1 Debugging Integration Tests
- Read the complete Jest failure output.
- Check the query and the actual rendered output.
- Verify that required providers and props are supplied.
- Inspect mocked functions and their return values.
- Check whether asynchronous updates are awaited.
- Run the failing test alone to isolate interference from other tests.
22.2 Debugging E2E Tests
- Confirm that the correct app build is installed.
- Verify that the simulator or emulator is running.
- Check whether the app launches successfully outside the test.
- Inspect the failed selector and the actual UI.
- Review application and automation logs.
- Capture screenshots or video where supported.
- Check backend availability and test data.
- Repeat the test under controlled conditions to identify intermittent failures.
22.3 Common Problems and Solutions
| Problem |
Possible Cause |
Recommended Solution |
| Integration test cannot find an element |
Incorrect query or conditional UI. |
Inspect the rendered output and use a suitable query. |
| API test fails unexpectedly |
Mock not configured correctly. |
Check mock setup, return values, and cleanup. |
| E2E test cannot locate a control |
Missing or incorrect test ID. |
Verify the actual rendered element and selector. |
| App does not launch |
Build configuration or simulator issue. |
Verify the build, device environment, and E2E configuration. |
| Test times out |
Expected condition never occurs or the environment is slow. |
Inspect app state, network requests, logs, and wait conditions. |
| Test passes locally but fails in CI |
Environment differences or uncontrolled dependencies. |
Align tool versions, test data, build configuration, and device settings. |
| Tests depend on order |
Shared state is not reset. |
Make each test independent and reset the relevant state. |
23. Running Tests in Continuous Integration
Continuous integration (CI) automatically runs selected checks when developers push code or create pull requests. It helps identify problems before changes are merged into the main branch.
23.1 Recommended CI Workflow
- Install dependencies using the project's lockfile.
- Run linting and static checks.
- Run unit and component tests.
- Run integration tests.
- Build the application for the selected platform.
- Run critical E2E tests on supported infrastructure.
- Collect test reports, logs, and failure artifacts.
- Report the result to the development team.
23.2 Example CI Test Commands
npm ci
npm test -- --runInBand
The commands above illustrate a basic dependency installation and Jest test run. The exact CI configuration depends on the project and platform. E2E tests usually require additional build, simulator, emulator, or device setup.
23.3 CI Best Practices
- Use consistent Node.js and package manager versions.
- Use the committed dependency lockfile.
- Keep test credentials in secure CI secrets when needed.
- Separate fast tests from longer device-based tests.
- Preserve logs and screenshots for failed E2E tests.
- Run critical regression tests before release.
- Monitor flaky tests and improve their reliability.
24. Best Practices for Integration and E2E Testing
- Choose tests by risk: Prioritize login, registration, checkout, payments, and other business-critical flows.
- Keep integration tests focused: Test meaningful interactions between related modules.
- Use real behavior where valuable: Avoid mocking the exact behavior the test is intended to verify.
- Mock external dependencies thoughtfully: Use controlled mocks for predictable service responses.
- Use stable selectors: Prefer accessible roles and names, or purposeful test IDs.
- Keep E2E workflows realistic: Follow actual user journeys.
- Isolate test data: Prevent one test from affecting another.
- Avoid unnecessary E2E tests: Use faster tests for cases that do not require a running app.
- Wait for conditions: Avoid arbitrary sleep calls wherever possible.
- Review failures carefully: Distinguish product defects from environment problems.
- Run tests in CI: Detect regressions before release.
- Keep tests maintainable: Refactor repeated setup into well-defined helpers.
- Test native behavior separately: Use device testing for features that JavaScript tests cannot fully verify.
25. Practical Project: Test a React Native Shopping Workflow
Build a small shopping application with a product list, a product detail screen, and a cart. Implement integration tests for component interactions and E2E tests for the complete shopping workflow.
25.1 Functional Requirements
- Display a list of products.
- Allow users to open a product detail screen.
- Display the selected product name and price.
- Allow users to add a product to the cart.
- Update the cart count after an item is added.
- Display a confirmation message or cart screen.
- Handle empty product data and relevant error states.
25.2 Suggested Test Cases
| Test Type |
Test Case |
Expected Result |
| Unit |
Calculate the cart total. |
The correct total is returned. |
| Component |
Render a product card. |
The product name and price appear. |
| Integration |
Add a product using the UI. |
The cart state and visible count update. |
| Integration |
Load products from a mocked service. |
The list displays the returned products. |
| E2E |
Open a product and add it to the cart. |
The cart displays the selected item. |
| E2E |
Open the cart with no items. |
The empty-cart message appears. |
| E2E |
Simulate a product loading failure. |
The appropriate error state appears. |
25.3 Example E2E Test
// e2e/shopping.e2e.js
describe('Shopping workflow', () => {
beforeEach(async () => {
await device.launchApp();
});
it('adds a product to the cart', async () => {
await expect(element(by.id('product-list')))
.toBeVisible();
await element(by.id('product-notebook')).tap();
await expect(element(by.id('product-detail-screen')))
.toBeVisible();
await element(by.id('add-to-cart-button')).tap();
await expect(element(by.id('cart-count')))
.toHaveText('1');
await element(by.id('open-cart-button')).tap();
await expect(element(by.id('cart-screen')))
.toBeVisible();
await expect(element(by.text('Notebook')))
.toBeVisible();
});
});
This test is an illustrative template. Ensure the app exposes the required selectors, starts with predictable cart data, and uses the test product expected by the scenario.
25.4 Project Deliverables
- A React Native shopping interface.
- Unit tests for cart calculations.
- Component tests for product cards and buttons.
- Integration tests for product loading and cart updates.
- E2E tests for adding a product and viewing the cart.
- A short test report documenting results and known limitations.
26. Practice Exercises
- Create an integration test for a registration form with required-field validation.
- Mock an authentication service and test successful and failed login attempts.
- Test a context provider and verify that changing shared state updates the UI.
- Test a product list with successful, empty, and failed data responses.
- Create an E2E test that launches the app and navigates from Home to Profile.
- Write an E2E test for a login flow using test credentials.
- Test a shopping cart workflow from product selection through cart confirmation.
- Simulate a failed API request and verify that the app displays an error message.
- Identify a flaky test and improve its selectors, synchronization, or test-data setup.
- Design a CI workflow that runs fast tests before the longer E2E suite.
27. Interview Questions and Answers
Q1. What is integration testing?
Integration testing verifies that multiple components, modules, or services work together correctly. It focuses on interactions and data flow between connected parts of the application.
Q2. What is end-to-end testing?
End-to-end testing verifies a complete application workflow from the user's perspective, usually by interacting with a running application in a simulator, emulator, or device environment.
Q3. What is the main difference between integration and E2E testing?
Integration testing usually focuses on a smaller group of connected modules, while E2E testing checks a complete workflow across the application.
Q4. Can Jest perform E2E testing by itself?
Jest can run JavaScript tests and is useful for unit and integration testing. It does not by itself provide the full device automation needed to exercise a complete React Native app. A compatible mobile automation tool is generally used for E2E testing.
Q5. What is React Native Testing Library used for?
It provides utilities for rendering components, querying elements, simulating user interactions, and asserting observable UI behavior.
Q6. What is Detox?
Detox is a mobile E2E testing framework used to automate React Native applications in supported environments and verify complete user workflows.
Q7. Why are test IDs useful?
Test IDs provide stable selectors for UI elements, especially when a meaningful accessibility query is not sufficient. They can make automated tests easier to maintain.
Q8. How should API calls be tested?
For component and integration tests, use controlled mocks or a test service to verify success, empty, loading, and failure behavior. Use a dedicated test backend where real service integration needs to be verified.
Q9. What is a flaky test?
A flaky test passes and fails inconsistently without a relevant code change. Common causes include timing issues, shared state, unstable selectors, and unreliable external dependencies.
Q10. How can flaky E2E tests be improved?
Use stable selectors, condition-based waiting, predictable test data, isolated workflows, and consistent simulator or emulator configurations.
Q11. Why is test-data isolation important?
It prevents tests from affecting one another and helps ensure that each test produces predictable results regardless of execution order.
Q12. Should every component have an E2E test?
No. Most isolated component behavior can be covered more efficiently by unit or component tests. E2E tests should focus on critical user journeys and cross-layer interactions.
Q13. How do you test navigation?
Use an integration test with the relevant navigator to verify screen transitions, or an E2E test to launch the app, perform navigation actions, and verify the destination screen.
Q14. What should happen when an API request fails?
The application should handle the failure gracefully, display an appropriate message or fallback, and provide retry behavior when relevant. Tests should verify that the failure state is visible and consistent.
Q15. What is continuous integration in testing?
Continuous integration automatically runs selected checks and tests when code changes are submitted. It helps teams identify regressions before merging or releasing changes.
28. Quick Revision Checklist
- Understand unit, component, integration, and E2E testing.
- Know when to use Jest and React Native Testing Library.
- Understand the purpose of a mobile E2E tool such as Detox.
- Test communication between components and services.
- Verify loading, success, empty, and error states.
- Mock external services when predictable responses are required.
- Use stable selectors for automated UI tests.
- Test complete user workflows such as login and shopping.
- Manage test data and application state carefully.
- Use condition-based waits rather than arbitrary delays.
- Investigate flaky tests and collect useful failure artifacts.
- Run suitable tests in a CI pipeline.
- Use real-device testing for native behaviors that test environments cannot fully reproduce.
29. Conclusion
Integration and end-to-end testing are important parts of developing dependable React Native applications. Integration tests verify that related components, services, and state-management systems cooperate correctly. E2E tests go further by validating complete workflows in a running application.
Using Jest and React Native Testing Library for focused integration tests, together with a compatible E2E automation framework, helps developers catch regressions at multiple levels. Stable selectors, controlled test data, proper asynchronous synchronization, and clear failure diagnostics are essential for a maintainable test suite.
Start with the application's most important workflows, add tests for common success and failure scenarios, and run the suite regularly. A thoughtful testing strategy improves reliability, reduces debugging time, and increases confidence when releasing new features.
Explore the course: React Native Training at JustAcademy
Register for a demo: Book a Course Demo
Visit the website: JustAcademy Official Website